﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

using Fasterflect;

using Preactor.Interop.PreactorObject;
using Preactor.ObjectModel.Attributes;
using Preactor.ObjectModel.BaseTypes;
using Preactor.ObjectModel.Entities;
using Preactor.ObjectModel.Interfaces;

namespace Preactor.ObjectModel
{
    using System.IO;
    using System.Runtime.Remoting.Messaging;

    using Preactor.ObjectModel.BaseTypes.MaterialControl;
    using Preactor.ObjectModel.Interfaces.MaterialControl;

    public class PreactorObjectModel
    {
        public PreactorObjectModel(bool registerDefaultTypes = true)
        {
            TypeRegister = new Dictionary<Type, Type>();
            FormatRecordDefinitions = new Dictionary<Type, Dictionary<string, RecordDefinition>>();
            FormatFieldPairDefinitions = new Dictionary<Type, Dictionary<string, FormatFieldPair>>();
            TypeToFormatNumbers = new Dictionary<Type, int>();
            TypeToFormatNames = new Dictionary<Type, string>();
            FormatNumberToNames = new Dictionary<int, string>();
            Cache = new Dictionary<Type, Dictionary<PrimaryKey, IPreactorRecord>>();
            IsPreactorObjectModelInitialized = false;
            TypePreactorFields = new Dictionary<Type, List<string>>();
            PropertiesCacheConfiguration = new PropertiesCacheConfiguration();

            Statistics = new Dictionary<string, int>();

            if (registerDefaultTypes)
            {
                RegisterDefaultTypes();
            }
        }

        public IPreactor Preactor { get; private set; }

        public IPlanningBoard PlanningBoard { get; private set; }

        // Type, Get/Set, PropertyName, Count
        internal Dictionary<string, int> Statistics{ get; set; }

        // Implementation is key
        public Dictionary<Type, List<string>> TypePreactorFields { get; private set; }

        // Implementation is key
        internal Dictionary<Type, Dictionary<string, FormatFieldPair>> FormatFieldPairDefinitions { get; private set; }

        internal bool IsPropertyCacheEnabled { get; set; }

        internal bool IsStatisticsEnabled { get; set; }

        private PropertiesCacheConfiguration PropertiesCacheConfiguration { get; set; }

        // Dictionary<Type, Dictionary< PropertyName, FormatFieldPair>>

        // Implementation is key
        private Dictionary<Type, Dictionary<PrimaryKey, IPreactorRecord>> Cache { get; set; }

        private Dictionary<Type, string> TypeToFormatNames { get; set; }

        private Dictionary<Type, int> TypeToFormatNumbers { get; set; }

        private Dictionary<int, string> FormatNumberToNames { get; set; } 

        private Dictionary<Type, Dictionary<string, RecordDefinition>> FormatRecordDefinitions { get; set; }

        private bool IsPreactorObjectModelInitialized { get; set; }

        private Dictionary<Type, Type> TypeRegister { get; set; }

        public void EnableMaterialControl()
        {
            Register<IDemand, Demand>("Demand");
            Register<ISupply, Supply>("Supply");
            Register<IBillOfMaterial, BillOfMaterial>("Bill of Materials");
            Register<ICoProduct, CoProduct>("Co-products");
            Register<IIgnoreShortage, IgnoreShortage>("Ignore Shortages");
            Register<IOrderLink, OrderLink>("Order Links");
            Register<IMaterialControlConfiguration, MaterialControlConfiguration>("Material Control Configuration");
            Register<IPeggingRuleSet, PeggingRuleSet>("Pegging Rule Set");
            Register<IPeggingRule, PeggingRule>("Pegging Rules");
        }

        public void EnablePropertyCache()
        {
            IsPropertyCacheEnabled = true;
        }

        public void DisablePropertyCache()
        {
            IsPropertyCacheEnabled = false;
        }

        public void EnableStatistics()
        {
            IsStatisticsEnabled = true;
        }

        public void DeleteRecord<T>(T preactorRecord) where T : IPreactorRecord
        {
            // Delete record from preactor. We have to assume Preactor was succcessful
            int recordNumber = Preactor.GetRecordNumber(preactorRecord.FormatName, preactorRecord.Id);
            Preactor.DeleteRecord(preactorRecord.FormatName, recordNumber);

            Cache[preactorRecord.GetType()].Remove(preactorRecord.Id);
        }

        public IEnumerable<TInterface> GetAllRecords<TInterface>() where TInterface : class, IPreactorRecord
        {
            var records = new List<TInterface>();

            // Get all the records
            int recordCount = Preactor.RecordCount(GetFormatNumber<TInterface>());

            for (int record = 1; record <= recordCount; record++)
            {
                records.Add(GetRecord<TInterface>(record));
            }

            return records;
        }

        public IEnumerable<Order> GetAllOrders()
        {
            return this.GetAllRecords<IOperation>().GroupBy(x => x.OrderNumber).Select(x => new Order(x));
        }

        public string GetPropertyName<T>(Expression<Func<T, object>> keySelector)
        {
            string propertyName;

            if (keySelector.Body is UnaryExpression)
            {
                propertyName = ((MemberExpression)((UnaryExpression)keySelector.Body).Operand).Member.Name;
            }
            else
            {
                propertyName = ((MemberExpression)keySelector.Body).Member.Name;
            }

            return propertyName;
        }

        public FormatFieldPair GetFormatFieldPair<TInterface>(Expression<Func<TInterface, object>> keySelector)
        {
            return GetFormatFieldPair(GetPropertyName(keySelector), ResolveType<TInterface>());
        }

        public FormatFieldPair GetFormatFieldPair(string propertyName, Type implementedType)
        {
            return FormatFieldPairDefinitions[implementedType][propertyName];
        }

        public string GetFormatName<TInterface>()
        {
            CheckInterfaceIsRegistered<TInterface>();

            Type type = ResolveType<TInterface>();

            return GetFormatNameByType(type);
        }

        public string GetFormatNameByType(Type implemetedType)
        {
            if (TypeToFormatNumbers.ContainsKey(implemetedType))
            {
                return TypeToFormatNames[implemetedType];
            }

            throw new ArgumentException(string.Format("The Format Number for the type : '{0}' is not defined.", implemetedType));
        }

        public int GetFormatNumber<TInterface>()
        {
            CheckInterfaceIsRegistered<TInterface>();

            Type type = ResolveType<TInterface>();

            return GetFormatNumberByType(type);
        }

        public int GetFormatNumberByType(Type implementedType)
        {
            if (TypeToFormatNumbers.ContainsKey(implementedType))
            {
                return TypeToFormatNumbers[implementedType];
            }

            throw new ArgumentException(string.Format("The Format Number for the type : '{0}' is not defined.", implementedType));
        }

        public TInterface GetRecord<TInterface>(int recordNumber) where TInterface : class, IPreactorRecord
        {
            // Get corresponding primary key
            FormatFieldPair formatFieldPair = GetFormatFieldPair("Id", ResolveType<TInterface>());
            int primaryKeyNumber = Preactor.ReadFieldInt(formatFieldPair, recordNumber);
            var pk = new PrimaryKey(primaryKeyNumber);

            return GetRecord<TInterface>(pk);
        }

        public TInterface CreateRecord<TInterface>() where TInterface : class, IPreactorRecord
        {
            int recordNumber = Preactor.CreateRecord(GetFormatName<TInterface>());
            return GetRecord<TInterface>(recordNumber);
        }

        public TInterface GetRecord<TInterface>(PrimaryKey pk) where TInterface : class, IPreactorRecord
        {
            IPreactorRecord recordT;
            Type type = ResolveType<TInterface>();

            if (Cache[type].ContainsKey(pk))
            {
                recordT = Cache[type][pk];
            }
            else
            {
                // Create new instance and add it to the Cache
                recordT = type.CreateInstance(pk, this) as IPreactorRecord;

                if (recordT == null)
                {
                    throw new ArgumentException(string.Format("Was not able to create a new instance for: {0}, with primary key: {1}", type, pk.Value));
                }

                // Add record to the cache
                Cache[type].Add(recordT.Id, recordT);
            }

           return recordT as TInterface;
        }

        public TInterface GetRecordByName<TInterface>(string name) where TInterface : class, IPreactorRecord
        {
            // The interface has to implement a "Name"
            // Only selects the first occurance, so "Name" should also be unique
            // TODO: Create a IName interface or something similar which indicates the name field is unique for that table.
            int record = 0;
            FormatFieldPair formatFieldPair = GetFormatFieldPair("Name", ResolveType<TInterface>());
            record = Preactor.FindMatchingRecord(formatFieldPair, record, name, SearchDirection.Forwards);

            if (record < 0)
            {
                return null;
            }

            return GetRecord<TInterface>(record);
        }

        public void LoadPreactorObject(PreactorObj preactorObject)
        {
            var iPreactor = PreactorFactory.CreatePreactorObject(preactorObject);
            this.LoadPreactorObject(iPreactor);
        }

        public void LoadPreactorObject(IPreactor iPreactor)
        {
            Preactor = iPreactor;
            PlanningBoard = Preactor.PlanningBoard;

            Initialise(); // We can only initialise once we have the PreactorObject, but we only want to initialise once.
        }

        /// <summary>
        /// static PreactorObjectModel
        /// The last type to be registered against an interface will win
        /// </summary>
        /// <typeparam name="TInterface"></typeparam>
        /// <typeparam name="TType"></typeparam>
        /// <param name="formatName"></param>
        public void Register<TInterface, TType>(string formatName = null) where TType : PreactorRecord, TInterface
        {
            if (!TypeRegister.ContainsKey(typeof(TInterface)))
            {
                TypeRegister.Add(typeof(TInterface), typeof(TType));
            }

            TypeRegister[typeof(TInterface)] = typeof(TType);
            BuildRecordDefinition(typeof(TType));

            Cache.Add(typeof(TType), new Dictionary<PrimaryKey, IPreactorRecord>());

            if (formatName != null)
            {
                TypeToFormatNames.Add(typeof(TType), formatName);
            }
        }

        public Type ResolveType<TInterface>()
        {
            return TypeRegister.ContainsKey(typeof(TInterface)) ? TypeRegister[typeof(TInterface)] : null;
        }

        /// <summary>
        /// Check whether a TDF field contains a given speficiation.
        /// Case sensitive.
        /// </summary>
        /// <param name="formatFieldPair"></param>
        /// <param name="classification"></param>
        /// <returns>True if there is an exact match</returns>
        public bool DoesFieldContainClassification(FormatFieldPair formatFieldPair, string classification)
        {
            var allClassificationStrings = Preactor.ClassificationString(formatFieldPair);

            var allClassifications = allClassificationStrings.Split(new [] { "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);

            if (allClassifications.Any(x => x.Trim() == classification))
            {
                return true;
            }

            return false;
        }

        public void ClearCacheForType(Type implementedType)
        {
            if (Cache.ContainsKey(implementedType))
            {
                Cache[implementedType].Clear();
            }
            else
            {
                throw new ArgumentException(string.Format("The type: {0}, is not defined in the Cache.", implementedType));
            }
        }

        public void PropertyCacheRegister<T>(IList<string> propertyNames)
        {
            PropertiesCacheConfiguration.SetCachedPropertyNames(typeof(T), propertyNames);
        }

        public void WriteStatisticsToFile(string fullPathName)
        {
            var content = new StringBuilder();

            Statistics.OrderByDescending(x => x.Value).ForEach(x => content.AppendFormat("{0} : {1}{2}", x.Key, x.Value, Environment.NewLine));

            File.WriteAllText(fullPathName, content.ToString());
        }

        internal IEnumerable<string> GetCachedProperties(Type implementedType)
        {
            return PropertiesCacheConfiguration.GetCachedPropertyNames(implementedType);
        }

        private void CheckInterfaceIsRegistered<TInterface>()
        {
            if (!TypeRegister.ContainsKey(typeof(TInterface)))
            {
                throw new ArgumentException("The interface " + typeof(TInterface).FullName + " does not have a registered type.");
            }
        }

        private void Initialise()
        {
            if (IsPreactorObjectModelInitialized)
            {
                return;
            }

            SetAllFormatNumberToNames();
            SetInterfaceFormatInformation();
            BuildFormatFieldPairDefinitions();
            Validate();

            IsPreactorObjectModelInitialized = true;
        }

        private void SetInterfaceFormatInformation()
        {
            var formatClassification = new FormatClassification();
            
            foreach (var type in TypeRegister.Keys)
            {
                // We have to make sure the type name is clean in case we have a generic type
                string fieldName = Regex.Replace(type.Name, "`.*", string.Empty);
                var fieldInfo = typeof(FormatClassification).GetField(fieldName);

                if (fieldInfo != null)
                {
                    string classification = fieldInfo.GetValue(formatClassification).ToString();
                    var formatFieldPair = Preactor.FindClassificationString(classification).SingleOrDefault();
                    int formatNumber = formatFieldPair.Equals(default(FormatFieldPair)) ? -1 : formatFieldPair.FormatNumber;
                    Type implementedType = TypeRegister[type];
                    TypeToFormatNumbers.Add(implementedType, formatNumber);
                }
            }
        }

        private void Validate()
        {
            this.ValidateResourceField();
        }

        private void SetAllFormatNumberToNames()
        {
            for (int i = 1; i < Preactor.FormatCount; i++)
            {
                var formatName = Preactor.GetFormatName(i);
                FormatNumberToNames.Add(i, formatName);

                Debug.WriteLine("Format number: " + i + " has name: " + formatName);
            }
        }

        private void BuildFormatFieldPairDefinitions()
        {
            var errorMessage = new StringBuilder();

            foreach (var formatRecordDefinition in FormatRecordDefinitions)
            {
                if (!TypeToFormatNumbers.ContainsKey(formatRecordDefinition.Key))
                {
                    // Check whether a type was registered with a format name
                    if (TypeToFormatNames.ContainsKey(formatRecordDefinition.Key))
                    {
                        TypeToFormatNumbers.Add(formatRecordDefinition.Key, Preactor.GetFormatNumber(TypeToFormatNames[formatRecordDefinition.Key]));
                    }
                    else
                    {
                        continue;
                    }
                }

                int formatNumber = TypeToFormatNumbers[formatRecordDefinition.Key];

                if (!TypeToFormatNames.ContainsKey(formatRecordDefinition.Key))
                {
                    TypeToFormatNames.Add(formatRecordDefinition.Key, formatNumber > 0 ? Preactor.GetFormatName(formatNumber) : "NONE");
                }

                FormatFieldPairDefinitions.Add(formatRecordDefinition.Key, new Dictionary<string, FormatFieldPair>());

                foreach (var recordDefinition in formatRecordDefinition.Value)
                {
                    var formatFieldPairInformation = GetFormatFieldPairInformation(recordDefinition.Value, formatNumber);

                    if (formatFieldPairInformation.Success)
                    {
                        FormatFieldPairDefinitions[formatRecordDefinition.Key].Add(recordDefinition.Key, formatFieldPairInformation.FormatFieldPair);
                    }
                    else
                    {
                        errorMessage.AppendLine(formatFieldPairInformation.ErrorMessage);
                    }
                }
            }

            if (errorMessage.Length > 0)
            {
                Logger.Log(errorMessage.ToString());
            }
        }

        private void BuildRecordDefinition(Type type)
        {
            FormatRecordDefinitions.Add(type, new Dictionary<string, RecordDefinition>());
            TypePreactorFields.Add(type, new List<string>());

            var properties = type.Properties();

            var propertyNames = new HashSet<string>();

            foreach (PropertyInfo property in properties)
            {
                object[] propertyAttributes = type.Property(property.Name).GetCustomAttributes(false);
                IEnumerable<PreactorFieldAttribute> preactorFieldAttributes = propertyAttributes.OfType<PreactorFieldAttribute>();

                if (!preactorFieldAttributes.Any())
                {
                    continue;
                }

                if (propertyNames.Contains(property.Name))
                {
                    throw new ArgumentException(string.Format("The property name: {0} in the type: {1}, is defined twice.", property.Name, type.FullName));
                }

                TypePreactorFields[type].Add(property.Name);

                PreactorFieldAttribute preactorFieldAttribute = propertyAttributes.OfType<PreactorFieldAttribute>().Single();
                string preactorField = preactorFieldAttribute.PreactorField;
                FieldDeclarationType fieldDeclarationType = preactorFieldAttribute.FieldDeclarationType;

                FormatRecordDefinitions[type].Add(property.Name, new RecordDefinition(preactorField, fieldDeclarationType));
                propertyNames.Add(property.Name);
            }
        }

        private FormatFieldPairInformation GetFormatFieldPairInformation(RecordDefinition recordDefinition, int formatNumber)
        {
            string field = recordDefinition.PreactorField;
            var formatFieldPair = new FormatFieldPair(-1, -1);

            string errorMessage = string.Empty;
            bool success = true;

            // If classification, find corresponding format field name
            if (recordDefinition.FieldDeclarationType == FieldDeclarationType.FieldClassification)
            {
                IEnumerable<FormatFieldPair> formatFieldPairs = Preactor.FindClassificationString(field);
                List<FormatFieldPair> query = formatFieldPairs.Where(x => x.FormatNumber == formatNumber).ToList();

                if (query.Count == 1)
                {
                    formatFieldPair = query.First();
                }
                else if(query.Count > 1)
                {
                    if (field == "PROCESS TIME")
                    {
                        // There is a possibility Preactor will return multiple formatFieldPairs with a classification containing PROCESS TIME,
                        // such as TOTAL PROCESS TIME etc... make sure we find the exact match.
                        foreach(var ffp in query)
                        {
                            if (DoesFieldContainClassification(ffp, field))
                            {
                                formatFieldPair = ffp;
                                break;
                            }
                        }

                        if(formatFieldPair.FieldNumber == -1)
                        {
                            // Should never really get here.
                            throw new ArgumentException("Could not find a classification match.");
                        }
                    }
                    else
                    {
                        formatFieldPair = query.First();

                        // NOTE: The order of the fields defined in the TDF are extremely important.

                        //errorMessage = string.Format("There are multiple classifications that contain the string '{0}' in format: {1}", field, Preactor.GetFormatName(formatNumber));
                        //success = false;
                    }
                }
                else
                {
                    errorMessage = string.Format("Cannot find a field with a classification of '{0}' in format: {1}", field, Preactor.GetFormatName(formatNumber));
                    success = false;
                }
            }
            else
            {
                // Check that the field name exist's
                int fieldNumber = Preactor.GetFieldNumber(formatNumber, field);

                if (fieldNumber > 0)
                {
                    formatFieldPair = new FormatFieldPair(formatNumber, fieldNumber);
                }
                else
                {
                    errorMessage = string.Format("Cannot find a field with name '{0}' in format: {1}", field, Preactor.GetFormatName(formatNumber));
                    success = false;
                }
            }

            return new FormatFieldPairInformation(formatFieldPair, success, errorMessage);
        }

        private void RegisterDefaultTypes()
        {
            // TODO: make this automated. i.e. Reflect on the loaded assemblies and register their types automatically.
            Register<IOperation, Operation>();
            
            // Register<IProduct, Product>();
            Register<IResource, Resource>();
            Register<IResourceGroup<IResource>, ResourceGroup>();
            Register<ISecondaryConstraint, SecondaryConstraint>();
        }
    }
}